/*-*-C-*-
 * Menu CSE protocol support
 */

#include "resed.h"
#include "main.h"

#include "swicall.h"
#include "wimp.h"
#include "resformat.h"
#include "newmsgs.h"
#include "registry.h"

#include "format.h"
#include "menuedit.h"
#include "props.h"
#include "protocol.h"


/* OUTGOING MESSAGES */


/*
 * Called whenever the user modifies a menu or a menu entry.
 * Checks the status of our private "modified" flag, and
 * sends a message to the shell if the flag is going from
 * FALSE to TRUE.
 */

error * protocol_send_resed_object_modified (MenuObjPtr menu)
{
    if (menu->modified == FALSE)
    {
        MessageResEdObjectModifiedRec modified;
        menu->modified = TRUE;
        modified.header.yourref = 0;
        modified.header.size = sizeof (modified);
        modified.header.messageid = MESSAGE_RESED_OBJECT_MODIFIED;
        modified.flags = 0;
        modified.documentID = menu->documentID;
        modified.objectID = menu->objectID;
        ER ( swi (Wimp_SendMessage,
                  R0, EV_USER_MESSAGE,
                  R1, &modified,
                  R2, parenttaskhandle,  END) );
    }
    return NULL;
}
        

/*
 * We are about to close an object.  Shell should clear it's "I am editing this
 * object" flag in response to this.  This function is called by
 * menuedit_close_menu().
 */

error * protocol_send_resed_object_closed (MenuObjPtr menu)
{
    MessageResEdObjectClosedRec closed;
    closed.header.yourref = 0;
    closed.header.size = sizeof (closed);
    closed.header.messageid = MESSAGE_RESED_OBJECT_CLOSED;
    closed.flags = 0;
    closed.documentID = menu->documentID;
    closed.objectID = menu->objectID;
    return swi (Wimp_SendMessage,
                R0, EV_USER_MESSAGE,
                R1, &closed,
                R2, parenttaskhandle,  END);
}


/*
 * We call this when we want to proactively send an object back to the shell,
 * normally when the user has pressed the window's CLOSE icon.
 * The shell will respond with RESED_OBJECT_LOADED, which is handled
 * by protocol_incoming_message().
 * If menu->pendingclose is set, then menuedit_close_menu(menu, TRUE) will
 * be called if and when the send is successful.
 *
 * NB this function does not check menu->modified.  The caller
 * can do so and bypass this function if wanted.
 */

error * protocol_send_resed_object_sending (MenuObjPtr menu)
{
    MessageResEdObjectSendingRec sending;
    int bodysize, stringsize, msgsize, numrelocs;
    int size = menuedit_object_size (menu, &bodysize, &stringsize, &msgsize, &numrelocs);
    error *err;

    if (menu->objectdata) free(menu->objectdata);
    menu->objectdata = malloc (size);
    if (menu->objectdata == NULL)
    {
        /* Hell!  We have nowhere to store the data to send it back.
         * Show the user an error message, and cancel the pending close flag.
         */
        menu->pendingclose = FALSE;
        return error_lookup ("NoMem");
    }
    else
    {
        err = menuedit_save_object_to_memory (menu, menu->objectdata, bodysize, stringsize, msgsize, numrelocs);
        if (err)
        {
            /* Something went wrong.  Free up the temp buffer, clear
             * the pendingclose flags and return an error
             */
            free (menu->objectdata);
            menu->objectdata = NULL;
            menu->pendingclose = FALSE;
            return err;
        }
    }
    sending.header.yourref = 0;
    sending.header.size = sizeof (sending);
    sending.header.messageid = MESSAGE_RESED_OBJECT_SENDING;
    sending.flags = 0;
    sending.documentID = menu->documentID;
    sending.objectID = menu->objectID;
    sending.address = (unsigned int) menu->objectdata;
    sending.size = size;        /* address and size valid only if flags == 0 */
    return swi (Wimp_SendMessage,
                R0, EV_USER_MESSAGE_RECORDED,
                R1, &sending,
                R2, parenttaskhandle,  END);
}
    

/*
 * We have received a DataSave message.  The only thing we accept is
 * something being dragged from our Document window of our parent shell.
 * We determine this by replying to the DataSave message with
 * a RESED_OBJECT_NAME_REQUEST message.  If the sender was our
 * shell it will reply with RESED_OBJECT_NAME, enabling us to
 * fill in a name field in our dbox.
 */

error * protocol_send_resed_object_name_request (MessageDataSavePtr save)
{
    MessageResEdObjectNameRequestRec req;
    MenuEntryPtr entry;

    /* ignore if not a drop onto a menu entry properties dbox */
    if (registry_lookup_window (save->windowhandle, (void **) &entry) != EntryDbox)
        return NULL;

    /* ignore unless the drop is onto an acceptable icon */
    if (!props_drop_icon(save->iconhandle))
        return NULL;

    req.header.yourref = save->header.myref;
    req.header.size = sizeof (req);
    req.header.messageid = MESSAGE_RESED_OBJECT_NAME_REQUEST;
    req.flags = 0;
    req.documentID = entry->owner->documentID;
    req.objectID = entry->owner->objectID;
    req.windowhandle = save->windowhandle;
    req.iconhandle = save->iconhandle;

    return swi (Wimp_SendMessage,
                R0, EV_USER_MESSAGE,
                R1, &req,
                R2, save->header.taskhandle,  END);
}



/* INCOMING MESSAGES */


/*
 * Use the document and object IDs from an incoming message to
 * locate our corresponding MenuObjPtr.  Return it, or NULL if not found.
 */

static MenuObjPtr locate_menu (Opaque documentID, Opaque objectID)
{
    int i = 0;
    RegistryType type;
    void *closure;

    while ((i = registry_enumerate_windows (i, &type, NULL, &closure)) != 0)
        if (type == MenuEdit)
        {
            MenuObjPtr menu = (MenuObjPtr) closure;
            if (menu->documentID == documentID && menu->objectID == objectID)
                return menu;
        }
    return NULL;
}


/*
 * The shell is asking us to open an object or raise it.  Fetch the
 * data using Wimp_TransferBlock and get it loaded into an editing
 * window.
 */

static error * received_resed_object_load (MessageResEdObjectLoadPtr load)
{
    MenuObjPtr menu = locate_menu (load->documentID, load->objectID);
    MessageResEdObjectLoadedPtr loaded = (MessageResEdObjectLoadedPtr) load;
    char *object = NULL;
    error *err;

    /*
     * This message should not have found us unless it is a Menu object.
     * Check, and refuse to load it if it isn't
     */

    if (load->class != MENU_OBJECT_CLASS)
    {
        loaded->flags = BIT(1);
        loaded->errcode = OBJECT_LOADED_ERROR_NONFATAL;
        err = error_lookup ("BadClass", load->class);
        goto reply;
    }

    /* If we already have this loaded, and the force bit is clear,
     * then just raise the menu editing window, and reply with
     * "success" regardless of whether an error occurred.
     */

    if (menu && (load->flags & BIT(0)) == 0)
    {
        err = menuedit_raise_window (menu);
        loaded->flags = 0;
        goto reply;
    }

    switch (load->version)
    {
    case 101:
    case MENU_OBJECT_VERSION:
        break;

    /* only the versions above can be handled by this version of !Menu */
    default:
        {
            char ver[20];
            sprintf (ver, "%d.%02d", load->version / 100, load->version % 100);
            loaded->flags = BIT(1);
            loaded->errcode = OBJECT_LOADED_ERROR_VERSION;
            err = error_lookup ("BadVersion", ver);
            goto reply;
        }
    }

    object = malloc (load->size);
    if (object == NULL)
    {
        loaded->flags = BIT(1);
        loaded->errcode = OBJECT_LOADED_ERROR_NOMEM;
        err = error_lookup ("NoMem");
        goto reply;
    }

    err = swi (Wimp_TransferBlock,
               R0, load->header.taskhandle,
               R1, load->address,
               R2, taskhandle,
               R3, object,
               R4, load->size,  END);
    if (err)
    {
        loaded->flags = BIT(0);
        loaded->errcode = OBJECT_LOADED_ERROR_NONFATAL;
        goto reply;
    }

    /* pre-process earlier object versions where necessary */
    switch (load->version)
    {
    case 101:    /* must add showevent and hideevent fields to menu */
        {
            int objsize = load->size;
            char *newobject;

            /* allocate space for version 102 object */
            newobject = malloc (objsize + 8);
            if (newobject == NULL)
            {
                loaded->flags = BIT(1);
                loaded->errcode = OBJECT_LOADED_ERROR_NOMEM;
                err = error_lookup ("NoMem");
                goto reply;
            }

            /* copy template to new space, with a gap for the new fields */
            {
                int brk = sizeof (ResourceFileObjectTemplateHeaderRec) +
                                     offsetof (MenuTemplateRec, showevent);

                memcpy (newobject, object, brk);
                memcpy (newobject + brk + 8, object + brk, objsize - brk);
            }

            {
                ResourceFileObjectTemplateHeaderPtr newobj =
                    (ResourceFileObjectTemplateHeaderPtr) newobject;
                MenuTemplatePtr newmenu = (MenuTemplatePtr) (newobj + 1);

                /* initialise new fields */
                newmenu->showevent = 0;
                newmenu->hideevent = 0;

                /* increment table offsets */
                if (newobj->stringtableoffset > 0)
                    newobj->stringtableoffset += 8;
                if (newobj->messagetableoffset > 0)
                    newobj->messagetableoffset += 8;
                if (newobj->relocationtableoffset > 0)
                    newobj->relocationtableoffset += 8;

                /* no change to bodyoffset, but both bodysize and totalsize
                    must be increased by 8 */
                newobj->hdr.bodysize += 8;
                newobj->hdr.totalsize += 8;

                /* scan relocation table:
                    any offsets beyond the new fields must be incremented */
                if (newobj->relocationtableoffset > 0)
                {
                    RelocationTablePtr relocs =(RelocationTablePtr)
                                (newobject + newobj->relocationtableoffset);
                    int numrelocations = relocs->numrelocations;
                    RelocationPtr reloc = relocs->relocations;
                    int i;

                    for (i = 0; i < numrelocations; i++)
                        if (reloc[i].wordtorelocate >=
                                offsetof (MenuTemplateRec, showevent))
                            reloc[i].wordtorelocate += 8;
                }
            }

            free (object);
            object = newobject;
        }
        break;
    }

    /*
     * If menu is NULL, then menuedit_load creates a new Menu structure.
     * If menu is non-NULL, it loads the new data into an existing one.
     * If it returns an error, the menu is not open
     */

    err = menuedit_load (menu, (ResourceFileObjectTemplateHeaderPtr) object, load);
    if (err)
    {
        loaded->flags = BIT(0);
        loaded->errcode = OBJECT_LOADED_ERROR_NONFATAL;
        goto reply;
    }

    loaded->flags = 0;          /* Success! */

reply:

    /*
     * If an error occurred, loaded->{flags, errcode} have already been
     * set up correctly.  Formulate the reply message in the same memory as the
     * incoming one.
     */

    free (object);
    loaded->header.yourref = load->header.myref;
    loaded->header.size = sizeof (MessageResEdObjectLoadedRec);
    loaded->header.messageid = MESSAGE_RESED_OBJECT_LOADED;
    (void) swi (Wimp_SendMessage,
                R0, EV_USER_MESSAGE,
                R1, loaded,
                R2, load->header.taskhandle,  END);

    /* Finally raise any error that we suffered, because the shell
     * will not tell the user.
     */
    return err;
}



/*
 * The shell is requesting the data associated with some object.
 * Send it along.
 */

static error * received_resed_object_send (MessageResEdObjectSendPtr send)
{
    MenuObjPtr menu = locate_menu (send->documentID, send->objectID);
    MessageResEdObjectSendingRec sending;
    int size = 0;
    error *err = NULL;

    if (menu == NULL)
    {
        err = error_lookup ("UnkObj");
        sending.errcode = OBJECT_SENDING_ERROR_UNKNOWN;
    }
    else
    {
        int bodysize, stringsize, msgsize, numrelocs;
        size = menuedit_object_size (menu, &bodysize, &stringsize, &msgsize, &numrelocs);

        if (menu->objectdata) free(menu->objectdata);
        menu->objectdata = malloc (size);
        if (menu->objectdata == NULL)
        {
            /* Hell!  We have nowhere to store the data to send it back.
             * Show the user an error message, and reply with an error
             * indication to the shell.
             */
            sending.errcode = OBJECT_SENDING_ERROR_NOMEM;
            err = error_lookup ("NoMem");
        }
        else
        {
            err = menuedit_save_object_to_memory (menu, menu->objectdata, bodysize, stringsize, msgsize, numrelocs);
            if (err)
            {
                free (menu->objectdata);
                menu->objectdata = NULL;
                sending.errcode = OBJECT_SENDING_ERROR_NONFATAL;
            }
        }
    }

    /* set 'pendingclose' flag if shell has requested that the object be
       deleted after a successful transfer */
    if (err == NULL && (send->flags & BIT(0)) != 0)
        menu->pendingclose = TRUE;

    /* Formulate reply */
    sending.header.size = sizeof(MessageResEdObjectSendingRec);
    sending.header.yourref = send->header.myref;
    sending.header.messageid = MESSAGE_RESED_OBJECT_SENDING;
    sending.flags = err ? BIT(0) : 0;
    sending.documentID = send->documentID;
    sending.objectID = send->objectID;
    sending.address = (unsigned int) menu->objectdata;
    sending.size = size;        /* address and size valid only if flags == 0 */
    (void) swi (Wimp_SendMessage,
                R0, err ? EV_USER_MESSAGE : EV_USER_MESSAGE_RECORDED,
                R1, &sending,
                R2, send->header.taskhandle,  END);

    return err;
}
    

/*
 * The shell has loaded the object we were sending it - or else generated an
 * error (in which case the user has already been told).  Free up our temporary
 * space and if the menu is waiting to be closed, then close it (unless there
 * was an error indication in the message).
 */

static error * received_resed_object_loaded (MessageResEdObjectLoadedPtr loaded)
{
    MenuObjPtr menu = locate_menu (loaded->documentID, loaded->objectID);

    if (menu)
    {
        if (menu->objectdata) free(menu->objectdata);
        menu->objectdata = NULL;

        /* Check the error indication.  If there was no error, then
         * go ahead and close the object if our pendingclose is set.
         */

        if ((loaded->flags & BIT(0)) == 0)
        {
            menu->modified = FALSE;
            if (menu->pendingclose)
            {
                /* The TRUE means "send a RESED_OBJECT_CLOSED message to the shell" */
                return menuedit_close_menu (menu, TRUE);
            }
        }
        menu->pendingclose = FALSE;
    }
    return  NULL;
}


/*
 * The shell is telling us that an object's name has changed.
 */

static error * received_resed_object_renamed (MessageResEdObjectRenamedPtr rename)
{
    MenuObjPtr menu = locate_menu (rename->documentID, rename->objectID);
    if (menu)
        return menuedit_rename_menu (menu, rename->name);
    else
        return NULL;
}


/*
 * The shell is telling us that an object it thinks we have open
 * is being deleted.  Close any window representing that menu,
 * and free up resources etc.  Do not reply to the message.
 */

static error * received_resed_object_deleted (MessageResEdObjectDeletedPtr delete)
{
    MenuObjPtr menu = locate_menu (delete->documentID, delete->objectID);
    if (menu)
        /* The FALSE means "don't send a RESED_OBJECT_CLOSED message to the shell" */
        return menuedit_close_menu (menu, FALSE);
    else
        return NULL;
}


/*
 * The shell is telling us the name of an object.  This will be in response
 * to us sending RESED_OBJECT_NAME_REQUEST in reply to a DataSave message.
 */

static error * received_resed_object_name (MessageResEdObjectNamePtr name)
{
    MenuObjPtr menu = locate_menu (name->documentID, name->objectID);
    if (name->flags == 0)
        return props_fill_in_object_name (menu, name->windowhandle, name->iconhandle, name->class, name->name);
    else
        return NULL;
}


/*
 * The shell has loaded new sprites.  Force a redisplay of all
 * the MenuEdit windows that are open
 */

static error * received_resed_sprites_changed (MessageResEdSpritesChangedPtr sprites)
{
    int i = 0;
    RegistryType type;
    void *closure;

    while ((i = registry_enumerate_windows (i, &type, NULL, &closure)) != 0)
        if (type == MenuEdit)
        {
            MenuObjPtr menu = (MenuObjPtr) closure;
            ER ( swi (Wimp_ForceRedraw,
                      R0,  menu->window->handle,
                      R1,  menu->window->workarea.minx,
                      R2,  menu->window->workarea.miny,
                      R3,  menu->window->workarea.maxx,
                      R4,  menu->window->workarea.maxy,  END) );
        }
    return NULL;
}


/*
 * The window editor has sent details of a keyboard shortcut to us.
 */

static error * received_resed_keycut_details (MessageResEdKeycutDetailsPtr keycut)
{
    if (keycut->shelltaskid == parenttaskhandle) /* must be from our shell */
    {
        MenuEntryPtr entry;
        int type = registry_lookup_window (keycut->windowhandle,
                                                    (void **) &entry);

        if (type == EntryDbox)    /* must be dropped onto an entry dbox */
        {
            ER ( props_enter_keycut_details (entry, keycut) );
        }
    }

    return NULL;
}


/*
 * Our attempt to send an object to the shell bounced.  Clear up
 * the temporary buffer that the object has allocated, and continue.
 */

static error * bounced_resed_object_sending (MessageResEdObjectSendingPtr sending)
{
    MenuObjPtr menu = locate_menu (sending->documentID, sending->objectID);
    if (menu)
    {
        if (menu->objectdata) free(menu->objectdata);
        menu->objectdata = NULL;
        menu->pendingclose = FALSE;
    }
    return NULL;
}


/*
 * Protocol message dispatcher.  The main event handling code uses this
 * to handle all ResEd shell<->CSE protocol messages.  Buf points
 * to a 64-word Wimp event buffer.  The event code is in 'event'.
 * Sets *handled to indicate to the caller whether the event has
 * been handled or not.
 */

error * protocol_incoming_message (int event, int *buf, Bool *handled)
{
    MessageHeaderPtr hdr = (MessageHeaderPtr) buf;
    if (handled) *handled = TRUE;
    switch (event)
    {
    case EV_USER_MESSAGE:
    case EV_USER_MESSAGE_RECORDED:
        switch (hdr->messageid)
        {
        case MESSAGE_RESED_OBJECT_LOAD:
            return received_resed_object_load ((MessageResEdObjectLoadPtr) buf);
        case MESSAGE_RESED_OBJECT_SEND:
            return received_resed_object_send ((MessageResEdObjectSendPtr) buf);
        case MESSAGE_RESED_OBJECT_LOADED:
            return received_resed_object_loaded ((MessageResEdObjectLoadedPtr) buf);
        case MESSAGE_RESED_OBJECT_RENAMED:
            return received_resed_object_renamed ((MessageResEdObjectRenamedPtr) buf);
        case MESSAGE_RESED_OBJECT_DELETED:
            return received_resed_object_deleted ((MessageResEdObjectDeletedPtr) buf);
        case MESSAGE_RESED_OBJECT_NAME:
            return received_resed_object_name ((MessageResEdObjectNamePtr) buf);
        case MESSAGE_RESED_SPRITES_CHANGED:
            return received_resed_sprites_changed ((MessageResEdSpritesChangedPtr) buf);
        case MESSAGE_RESED_KEYCUT_DETAILS:
            return received_resed_keycut_details ((MessageResEdKeycutDetailsPtr) buf);
        }
        break;
    case EV_USER_MESSAGE_ACKNOWLEDGE:
        switch (hdr->messageid)
        {
        case MESSAGE_RESED_OBJECT_SENDING:
            return bounced_resed_object_sending ((MessageResEdObjectSendingPtr) buf);
        }
        break;
    }
    if (handled) *handled = FALSE;
    return NULL;
}
